import { Injectable } from '@nestjs/common';
import { CreateMenuDto } from './dto/create-menu.dto';
import { UpdateMenuDto } from './dto/update-menu.dto';
import { InjectRepository } from '@nestjs/typeorm';
import { Menu } from './entities/menu.entity';
import { DataSource, In, Repository } from 'typeorm';
import { RoutesVo } from './vo/routes.vo';
import { MenuVo } from './vo/menu.vo';
import { buildWhere, isObjValid } from '../../../util/common-fn';

type FilterFnType = (a: Menu) => boolean;
// 目前支持的排序
type SortFnType = (a: Menu, b: Menu) => number;
// 默认排序方式
const defaultSort: SortFnType = (a, b) => a.sort - b.sort;

@Injectable()
export class MenuService {
  constructor(
    @InjectRepository(Menu)
    private menuRepository: Repository<Menu>,
    private dataSource: DataSource,
  ) {}

  /**
   * 创建菜单
   * @param createMenuDto
   */
  async create(createMenuDto: CreateMenuDto) {
    // 新增的时候,parent的type只能是1菜单和2目录
    const parent = await this.menuRepository.findOneBy({
      id: createMenuDto.parent.id,
    });
    // 如果传入的父节点有值,但是数据库中查找不到
    if (createMenuDto.parent.id && !parent) {
      throw new Error('父节点不存在');
    }
    createMenuDto.parent = parent;
    // 如果找得到父节点且 父节点的类型是1菜单或2目录
    // 另外一种是找不到父节点,是根节点,可以直接创建
    if (!parent || (parent && [1, 2].includes(parent.type))) {
      await this.menuRepository.save(createMenuDto);
    } else {
      throw new Error('父节点必须是菜单或者目录');
    }
  }

  /**
   * 菜单首页展示:
   *  如果没有查询条件,就以树状图显示
   *  如果有查询条件,就以普通表格显示,带缓存
   * @param query
   */
  async findAll(query: Partial<CreateMenuDto>): Promise<MenuVo[]> {
    // 如果没有查询条件,我就返回树状结构
    if (!isObjValid(query)) {
      return this.sortTree(
        await this.menuRepository.manager.getTreeRepository(Menu).findTrees(),
      );
    } else {
      // 如果有查询条件,我就返回平面结构
      const whereClause = buildWhere(query);
      return await this.menuRepository.find({
        where: whereClause,
        cache: true,
        // order: { sort: 'asc' },
      });
    }
  }

  /**
   * 通过id获取菜单信息,包含parent
   * @param id
   */
  async findOneWithParent(id: number): Promise<MenuVo> {
    return await this.menuRepository.findOne({
      where: { id },
      relations: { parent: true },
    });
  }

  /**
   * 通过ids获取菜单信息,包含parent
   * @param ids
   */
  async findManyWithChildren(ids: number[]): Promise<MenuVo[]> {
    return await this.menuRepository.find({
      where: { id: In(ids) },
      relations: { children: true },
    });
  }

  /**
   * 修改菜单(菜单类型(1:菜单；2:目录；3:按钮；4:外链))
   * 规则如下:
   *  如果自己的type是[1,2,4],可以改到parent的type为[2]-目录下面或者parent不存在也行(表示根目录)
   *  如果自己的type是[3]-按钮,可以改到到parent的type为[1]-菜单下面
   *
   */
  async update(id: number, updateMenuDto: UpdateMenuDto) {
    const { parent, ...restMenu } = updateMenuDto;
    const parentMenu = await this.menuRepository.findOneBy({
      id: parent.id,
    });
    if (
      ([1, 2, 4].includes(updateMenuDto.type) &&
        (!parentMenu || [2].includes(parentMenu.type))) ||
      ([3].includes(updateMenuDto.type) && [1].includes(parentMenu.type))
    ) {
      await this.dataSource.transaction(async (manager) => {
        // 首先更新自己的信息
        await manager.update(Menu, { id }, restMenu);
        // 找到更新后的数据
        const menu = await manager.findOneBy(Menu, { id });
        // 然后设置自己的parent
        menu.parent = parentMenu;
        // save的时候,如果parent的id发生了变化,就更新parent,如果相同就不改变
        await manager.save(Menu, menu);
      });
    } else {
      throw new Error('1.菜单,目录,外链只能改到目录下\n 2.按钮只能改到菜单下');
    }
  }

  /**
   * 删除菜单:
   *  为了防止错误,不使用级联删除,只能删除单个菜单
   * @param id
   */
  async remove(id: number) {
    // 规定根节点不能删除
    const currentMenu = await this.menuRepository.findOneByOrFail({ id });
    const count = await this.menuRepository.manager
      .getTreeRepository(Menu)
      .countDescendants(currentMenu);
    if (count > 1) {
      throw new Error('只能删除单个子节点');
    }
    const result = await this.menuRepository.softDelete({ id });
    return result.affected;
  }

  /**
   * 通过ids找到对应的menu
   * @param ids
   */
  findByIds(ids: number[]) {
    return this.menuRepository.find({
      where: {
        id: In(ids),
      },
    });
  }

  /**
   * 获取左边显示的路由
   * 需要说明的是:ROOT用户不受状态影响,可以看到所有路由
   * @param isRoot
   */
  async getRoutes(isRoot: boolean) {
    const menus = await this.menuRepository.manager
      .getTreeRepository(Menu)
      .findTrees({ relations: ['roles'] });
    // 过滤出状态为1的,且类型是1菜单或2目录
    const filteredMenus = this.filterTree(
      menus,
      (menu) => (isRoot || menu.status === 1) && [1, 2].includes(menu.type),
    );
    // 然后组装成前端需要的
    return this.handleMenusTree(this.sortTree(filteredMenus));
  }

  /**
   * 为路由处理menu
   * @param menusTree
   * @private
   */
  private handleMenusTree(menusTree: Menu[]) {
    const routesVos: RoutesVo[] = [];
    menusTree.forEach((menu) => {
      const routesVo = new RoutesVo();
      routesVo.path = menu.path;
      routesVo.name = menu.path + menu.id;
      const { component, redirect, children } = menu;
      if (component) {
        routesVo.component = component;
      }
      if (redirect) {
        routesVo.redirect = redirect;
      }
      routesVo.meta = {
        title: menu.name,
        icon: menu.icon,
        hidden: !!menu.status,
        roles: menu.roles.map((role) => role.code),
        keepAlive: true,
      };
      if (Array.isArray(children) && children.length > 0) {
        routesVo.children = this.handleMenusTree(menu.children);
      }
      routesVos.push(routesVo);
    });
    return routesVos;
  }

  /**
   * 获取菜单的树状结构(只包含菜单和目录)
   */
  async findForSelect(filterFn?: FilterFnType) {
    // 由于findTrees()不提供过滤,所以需要手动过滤,筛选出type = [1,2]的
    let menuTrees = await this.menuRepository.manager
      .getTreeRepository(Menu)
      .findTrees();
    if (filterFn) {
      menuTrees = this.filterTree(menuTrees, filterFn);
    }
    return this.sortTree(menuTrees);
  }

  /**
   * 过滤树(给出过滤条件来过滤树)
   * @param trees 树状结构的Menu
   * @param filterFn 过滤方法:(a: Menu) => boolean
   */
  private filterTree(trees: Menu[], filterFn: FilterFnType): Menu[] {
    const tempTrees: Menu[] = [];
    trees.forEach((tree) => {
      let tempTree = new Menu();
      if (filterFn(tree)) {
        tempTree = { ...tree, children: [] };
        tempTree.children = this.filterTree(tree.children, (innerTree) =>
          filterFn(innerTree),
        );
        tempTrees.push(tempTree);
      }
    });
    return tempTrees;
  }

  /**
   * 排序树(给出排序条件来排序树
   * (a: Menu, b: Menu) => number  )
   * @param trees
   * @param sortFn
   */
  private sortTree(trees: Menu[], sortFn: SortFnType = defaultSort) {
    trees.sort(sortFn);
    trees.forEach((tree) => {
      const tempTree: Menu = { ...tree, children: [] };
      tempTree.children = this.sortTree(tree.children, sortFn);
    });
    return trees;
  }
}
